package ctu.kfad.run.memory;

import ctu.kfad.run.ClassFileRepository;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;

import java.util.*;

public class ClassSizeDescriptor {

    public static Set<String> skipClasses = new HashSet<>();

    static {
        skipClasses.add("java.lang.Object");
        skipClasses.add("java.lang.String");
    }

    public final static String SIGNATURE_INTEGER = "I";
    public final static String SIGNATURE_INTEGER_ARRAY = "[I";
    public final static String SIGNATURE_BOOLEAN = "Z";
    public final static String SIGNATURE_BOOLEAN_ARRAY = "[Z";
    public final static String SIGNATURE_STRING = "Ljava/lang/String;";
    public final static String SIGNATURE_REFERENCE = "L";          // fully-qualified-class
    public final static String SIGNATURE_REFERENCE_ARRAY = "[L";

    private final Map<FieldClassHolder, Integer> size = new HashMap<>();
    private final Map<FieldClassHolder, Integer> offset = new HashMap<>();
    private final Map<FieldClassHolder, String> signature = new HashMap<>();
    private final Map<FieldClassHolder, Boolean> isPublic = new HashMap<>();
    private final Map<String, FieldClassHolder> publicKeys = new HashMap<>();
    private final Set<Integer> referenceOffset = new HashSet<>();
    private final int totalSize;

    public ClassSizeDescriptor(String className, ClassFileRepository repository) {
        final LinkedList<JavaClass> classNames = getClassesOrderedFromSuperDesc(className, repository);

        int totalSize = 0;
        for (JavaClass classToSearch : classNames) {
            totalSize = searchFieldsInClass(classToSearch, totalSize);
        }
        this.totalSize = totalSize;
    }

    private LinkedList<JavaClass> getClassesOrderedFromSuperDesc(String className, ClassFileRepository repository) {
        final LinkedList<JavaClass> classNames = new LinkedList<>();
        final JavaClass clazz = repository.loadClass(className);
        classNames.addFirst(repository.loadClass(className));
        String superclassName = clazz.getSuperclassName();
        while (superclassName != null && !skipClasses.contains(superclassName)) {
            final JavaClass superClazz = repository.loadClass(superclassName);
            classNames.addFirst(superClazz);
            superclassName = superClazz.getSuperclassName();
        }
        return classNames;
    }

    private int searchFieldsInClass(JavaClass clazz, int intOffset) {
        final Field[] fields = clazz.getFields();
        for (int i = 0; i < fields.length; i++) {
            final Field field = fields[i];
            final FieldClassHolder key = new FieldClassHolder(field.getName(), clazz.getClassName(), null);
            isPublic.put(key, new Boolean(field.isPublic()));
            if (field.isPublic()) {
                publicKeys.put(field.getName(), key);
            }
            offset.put(key, new Integer(intOffset));
            final String fieldSignature = field.getSignature();
            switch (fieldSignature) {
                case SIGNATURE_INTEGER:
                    intOffset = processPrimitive(intOffset, key, SIGNATURE_INTEGER, FieldValueConverter.INT_BYTE_SIZE);
                    break;
                case SIGNATURE_BOOLEAN:
                    intOffset = processPrimitive(intOffset, key, SIGNATURE_BOOLEAN, FieldValueConverter.BYTE_BYTE_SIZE);
                    break;
                case SIGNATURE_STRING:
                    intOffset = processReference(intOffset, key, SIGNATURE_STRING);
                    break;
                case SIGNATURE_INTEGER_ARRAY:
                    intOffset = processReference(intOffset, key, SIGNATURE_INTEGER_ARRAY);
                    break;
                case SIGNATURE_BOOLEAN_ARRAY:
                    intOffset = processReference(intOffset, key, SIGNATURE_BOOLEAN_ARRAY);
                    break;
                default:
                    if (fieldSignature.startsWith(SIGNATURE_REFERENCE)) {
                        intOffset = processReference(intOffset, key, SIGNATURE_REFERENCE);
                    } else if (fieldSignature.startsWith(SIGNATURE_REFERENCE_ARRAY)) {
                        intOffset = processReference(intOffset, key, SIGNATURE_REFERENCE_ARRAY);
                    } else {
                        System.out.println();
                        throw new RuntimeException("Signature " + field.getSignature() + " is not supported");
                    }
            }
        }
        return intOffset;
    }

    private int processReference(int intOffset, FieldClassHolder key, String signatureString) {
        size.put(key, FieldValueConverter.REFERENCE_BYTE_SIZE);
        signature.put(key, signatureString);
        referenceOffset.add(new Integer(intOffset));
        intOffset += FieldValueConverter.REFERENCE_BYTE_SIZE;
        return intOffset;
    }

    private int processPrimitive(int intOffset, FieldClassHolder key, String signature, int sizeInBytes) {
        size.put(key, sizeInBytes);
        this.signature.put(key, signature);
        intOffset += sizeInBytes;
        return intOffset;
    }

    public int getTotalSize() {
        return totalSize;
    }

    public MemoryFieldInfo getInfo(String fieldName, String className) {
        FieldClassHolder key = new FieldClassHolder(fieldName, className, null);
        final FieldClassHolder publicKeyForField = publicKeys.get(fieldName);

        if (publicKeyForField != null) {
            //it is probably public but we need to be sure - it can be private
            final Boolean publicBoolean = isPublic.get(key);
            if (publicBoolean == null || publicBoolean == true) {
                //null we can call it from other class where it is not defined
                key = publicKeyForField;
            }
        }

        if (key == null) {
            throw new RuntimeException("unknown combination for fieldName:[" + fieldName + "] and className:[" + className + "]");
        }

        final Integer s = size.get(key);
        final Integer o = offset.get(key);
        final String sig = signature.get(key);
        return new MemoryFieldInfo(o, s, sig);
    }

    public Set<Integer> getReferenceOffset() {
        return referenceOffset;
    }

}
